/*
 * Copyright 2014 Semidata. All rights reserved.
 */
package com.semidata.rtd.core.redis;

/**
 * @author shewei@semidata.com, She Wei
 *
 */
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

import java.util.ArrayList;
import java.util.List;


public final class RedisProtocol {
	public static final int DEFAULT_PORT = 6379;
	public static final int DEFAULT_SENTINEL_PORT = 26379;
	public static final int DEFAULT_TIMEOUT = 2000;
	public static final int DEFAULT_DATABASE = 0;

	public static final String CHARSET = "UTF-8";

	public static final byte DOLLAR_BYTE = '$';
	public static final byte ASTERISK_BYTE = '*';
	public static final byte PLUS_BYTE = '+';
	public static final byte MINUS_BYTE = '-';
	public static final byte COLON_BYTE = ':';

	public static final String SENTINEL_MASTERS = "masters";
	public static final String SENTINEL_GET_MASTER_ADDR_BY_NAME = "get-master-addr-by-name";
	public static final String SENTINEL_RESET = "reset";
	public static final String SENTINEL_SLAVES = "slaves";
	public static final String SENTINEL_FAILOVER = "failover";
	public static final String SENTINEL_MONITOR = "monitor";
	public static final String SENTINEL_REMOVE = "remove";
	public static final String SENTINEL_SET = "set";

	public static final String CLUSTER_NODES = "nodes";
	public static final String CLUSTER_MEET = "meet";
	public static final String CLUSTER_ADDSLOTS = "addslots";
	public static final String CLUSTER_DELSLOTS = "delslots";
	public static final String CLUSTER_INFO = "info";
	public static final String CLUSTER_GETKEYSINSLOT = "getkeysinslot";
	public static final String CLUSTER_SETSLOT = "setslot";
	public static final String CLUSTER_SETSLOT_NODE = "node";
	public static final String CLUSTER_SETSLOT_MIGRATING = "migrating";
	public static final String CLUSTER_SETSLOT_IMPORTING = "importing";
	public static final String CLUSTER_SETSLOT_STABLE = "stable";
	public static final String CLUSTER_FORGET = "forget";
	public static final String CLUSTER_FLUSHSLOT = "flushslots";
	public static final String CLUSTER_KEYSLOT = "keyslot";
	public static final String CLUSTER_COUNTKEYINSLOT = "countkeysinslot";
	public static final String CLUSTER_SAVECONFIG = "saveconfig";
	public static final String CLUSTER_REPLICATE = "replicate";
	public static final String CLUSTER_SLAVES = "slaves";
	public static final String CLUSTER_FAILOVER = "failover";
	public static final String CLUSTER_SLOTS = "slots";
	public static final String PUBSUB_CHANNELS = "channels";
	public static final String PUBSUB_NUMSUB = "numsub";
	public static final String PUBSUB_NUM_PAT = "numpat";

	private RedisProtocol() {
		// this prevent the class from instantiation
	}

	public static byte[] sendCommand(final Command command,
	        final byte[]... args) {
		return sendCommand(command.raw, args);
	}

	private static byte[] sendCommand(final byte[] command,
	        final byte[]... args) {
		ByteBuf buffer = Unpooled.buffer();

		buffer.writeByte(ASTERISK_BYTE);
		buffer.writeBytes(String.valueOf(args.length + 1).getBytes());
		buffer.writeByte('\r');
		buffer.writeByte('\n');

		buffer.writeByte(DOLLAR_BYTE);
		buffer.writeBytes(String.valueOf(command.length).getBytes());
		buffer.writeByte('\r');
		buffer.writeByte('\n');
		buffer.writeBytes(command);
		buffer.writeByte('\r');
		buffer.writeByte('\n');

		for (final byte[] arg : args) {
			buffer.writeByte(DOLLAR_BYTE);
			buffer.writeBytes(String.valueOf(arg.length).getBytes());
			buffer.writeByte('\r');
			buffer.writeByte('\n');
			buffer.writeBytes(arg);
			buffer.writeByte('\r');
			buffer.writeByte('\n');
		}
		buffer.writeByte('\r');
		buffer.writeByte('\n');
		byte[] ret = new byte[buffer.readableBytes()];
		buffer.readBytes(ret);
		return ret;
	}

	private static String processError(DataBuffer buffer) {
		return buffer.readLine();
	}

	private static Object process(DataBuffer buffer) {

		byte[] b = buffer.readBytes(1);
		if (b == null) {
			buffer.rollback();
			return null;
		}

		Object ret = null;

		if (b[0] == MINUS_BYTE) {
			String msg = processError(buffer);
			if (msg != null) {
				ret = new RedisCommandException(msg);
			}
		} else if (b[0] == ASTERISK_BYTE) {
			ret = processMultiBulkReply(buffer);
		} else if (b[0] == COLON_BYTE) {
			ret = processInteger(buffer);
		} else if (b[0] == DOLLAR_BYTE) {
			ret = processBulkReply(buffer);
		} else if (b[0] == PLUS_BYTE) {
			ret = processStatusCodeReply(buffer);
		} else {
			throw new RedisFatalException("Unknown reply: " + (char) b[0]);
		}

//		if (ret == null) {
//			buffer.rollback();
//		}
		return ret;
	}

	private static byte[] processStatusCodeReply(DataBuffer buffer) {
		String str = buffer.readLine();
		if (str == null)
			return null;
		return str.getBytes();
	}

	private static byte[] processBulkReply(DataBuffer buffer) {

		String str = buffer.readLine();
		if (str == null)
			return null;

		int len = Integer.parseInt(str);
		if (len == -1) {
			return new byte[0]; // means null
		}
		byte[] bytes = buffer.readBytes(len);

		if (bytes == null) {
			return null;
		}

		if (buffer.readBytes(2) == null)
			return null;
		return bytes;
	}

	private static Long processInteger(DataBuffer buffer) {
		String num = buffer.readLine();
		if (num == null)
			return null;
		return Long.valueOf(num);
	}

	private static List<Object> processMultiBulkReply(DataBuffer buffer) {
		String str = buffer.readLine();
		if (str == null)
			return null;
		int num = Integer.parseInt(str);
		if (num == -1) {
			return new ArrayList<>();
		}

		List<Object> ret = new ArrayList<Object>(num);
		for (int i = 0; i < num; i++) {
			try {
				Object obj = process(buffer);
				if (obj == null)
					return null;
				ret.add(obj);
			} catch (RedisCommandException e) {
				ret.add(e);
			}
		}
		return ret;
	}

	public static Object read(DataBuffer buffer) {
		return process(buffer);
	}

	public static final byte[] toByteArray(final boolean value) {
		return toByteArray(value ? 1 : 0);
	}

	public static final byte[] toByteArray(final int value) {
		return String.valueOf(value).getBytes();
	}

	public static final byte[] toByteArray(final long value) {
		return String.valueOf(value).getBytes();
	}

	public static final byte[] toByteArray(final double value) {
		return String.valueOf(value).getBytes();
	}

	public static enum Command {
		PING, SET, GET, QUIT, EXISTS, DEL, TYPE, FLUSHDB, KEYS, RANDOMKEY, RENAME, RENAMENX, RENAMEX, DBSIZE, EXPIRE, EXPIREAT, TTL, SELECT, MOVE, FLUSHALL, GETSET, MGET, SETNX, SETEX, MSET, MSETNX, DECRBY, DECR, INCRBY, INCR, APPEND, SUBSTR, HSET, HGET, HSETNX, HMSET, HMGET, HINCRBY, HEXISTS, HDEL, HLEN, HKEYS, HVALS, HGETALL, RPUSH, LPUSH, LLEN, LRANGE, LTRIM, LINDEX, LSET, LREM, LPOP, RPOP, RPOPLPUSH, SADD, SMEMBERS, SREM, SPOP, SMOVE, SCARD, SISMEMBER, SINTER, SINTERSTORE, SUNION, SUNIONSTORE, SDIFF, SDIFFSTORE, SRANDMEMBER, ZADD, ZRANGE, ZREM, ZINCRBY, ZRANK, ZREVRANK, ZREVRANGE, ZCARD, ZSCORE, MULTI, DISCARD, EXEC, WATCH, UNWATCH, SORT, BLPOP, BRPOP, AUTH, SUBSCRIBE, PUBLISH, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBSUB, ZCOUNT, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZUNIONSTORE, ZINTERSTORE, ZLEXCOUNT, ZRANGEBYLEX, ZREMRANGEBYLEX, SAVE, BGSAVE, BGREWRITEAOF, LASTSAVE, SHUTDOWN, INFO, MONITOR, SLAVEOF, CONFIG, STRLEN, SYNC, LPUSHX, PERSIST, RPUSHX, ECHO, LINSERT, DEBUG, BRPOPLPUSH, SETBIT, GETBIT, BITPOS, SETRANGE, GETRANGE, EVAL, EVALSHA, SCRIPT, SLOWLOG, OBJECT, BITCOUNT, BITOP, SENTINEL, DUMP, RESTORE, PEXPIRE, PEXPIREAT, PTTL, INCRBYFLOAT, PSETEX, CLIENT, TIME, MIGRATE, HINCRBYFLOAT, SCAN, HSCAN, SSCAN, ZSCAN, WAIT, CLUSTER, ASKING, PFADD, PFCOUNT, PFMERGE;

		public final byte[] raw;

		Command() {
			raw = this.name().getBytes();
		}
	}

	public static enum Keyword {
		AGGREGATE, ALPHA, ASC, BY, DESC, GET, LIMIT, MESSAGE, NO, NOSORT, PMESSAGE, PSUBSCRIBE, PUNSUBSCRIBE, OK, ONE, QUEUED, SET, STORE, SUBSCRIBE, UNSUBSCRIBE, WEIGHTS, WITHSCORES, RESETSTAT, RESET, FLUSH, EXISTS, LOAD, KILL, LEN, REFCOUNT, ENCODING, IDLETIME, AND, OR, XOR, NOT, GETNAME, SETNAME, LIST, MATCH, COUNT;
		public final byte[] raw;

		Keyword() {
			raw = this.name().toLowerCase().getBytes();
		}
	}
	
	public static void main(String[] args) {
		
		String data1 = "*10\r\n$1\r\na\r\n$2\r\n";
		
		DataBuffer buffer = new DataBuffer();
		
		buffer.addData(data1.getBytes());
		
		Object ret = process(buffer);
		
		System.out.println(ret);
	}
}
